/*
 * Copyright (C) 2013 rdrrlabs gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package serv

import (
    "appengine"
    "appengine/datastore"
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "html/template"
    "log"
    "net/http"
    "net/url"
    "reflect"
    "strconv"
    "strings"
    "time"
)

//------------------------------

// "callbacks" implemented by "derived" cruders
type Cruder interface {
    // Register handlers specific to that cruder (typicall the main view page)
    RegisterHandlers(ci *CrudInfo, handlers Handlers) Handlers

    // --- ui tab ---

    // Return true if it's OK to create a UI tab page for this request.
    CanCreateTab(p *CrudHandler, r *http.Request, c appengine.Context, a *AccessInfo) bool

    // --- get list ---

    // --- add/edit ---

    // Validates and prepare an add/edit request (a PUT/POST).
    // - values: the parameters given by the UI POST.
    // - entity: the struct to place in the datastore. MUST be consistent with the DsStruct type and
    //           one of the fields must marked with the tag `crud:"key"`
    // - err: an optional error. nil on success. Will be part of the HTTP response, so no PII.
    ValidateAdd(values url.Values) (entity Datastorer, err error)
}

// Optional interface that Cruder can implement to use ProcessRequest
// on non-Crud requests.
type CruderProcessRequester interface {
    // Process UI-less request
    ProcessRequest(w http.ResponseWriter,
                   r *http.Request,
                   c appengine.Context,
                   a *AccessInfo,
                   urlBase string,
                   urlRest string) (httpCode int, err error)
}

// The struct type for AppEngine datastore must implement this interface.
// One field should have the tag `crud:"key"`
type Datastorer interface {
    // Converts a DataStore entity into an array of strings matching
    // the fields expected in the CRUD html data table.
    ToJsonTable() []string
}

// info struct for the generic crud code
type CrudInfo struct {
    Name            string      // e.g. "Name", the capitalized title of the pages
    BaseUrl         string      // e.g. "/a" will convert to "/a/get"
    Kind            string      // e.g. "name", the lower-case AppEngine datastore kind (str)
    TemplateName    string      // e.g. "name_template.html", based on the lower-case name
    DsStruct        Datastorer  // the struct type for AppEngine datastore; one field should have the tag `crud:"key"`
    Cruder
}

// bridge between page/request handlers and the generic crud
type CrudHandler struct {
    *HandlerDesc
    *CrudInfo
}

// Creates a new Crud struct. Caller should call RegisterHandlers on it later.
func NewCrud(name, baseUrl string, dsStruct Datastorer, c Cruder) *CrudInfo {

    kname := strings.ToLower(name)
    kname  = strings.Replace(kname, " ", "_", -1)

    ci := &CrudInfo {
            Cruder:         c,
            Name:           name,
            BaseUrl:        baseUrl,
            Kind:           kname,
            TemplateName:   kname + "_template.html",
            DsStruct:       dsStruct,
          }

    return ci
}

func (ci *CrudInfo) RegisterHandlers(handlers Handlers) Handlers {
    desc := &HandlerDesc { Title:  ci.Name,
                           URL:    ci.BaseUrl + "/get",
                           Flags:  HandlerIsSigned,
                           Groups: []string { GroupView, GroupAdmin } }
    handlers = append(handlers, &CrudHandler { desc, ci })

    desc  = &HandlerDesc { Title:  ci.Name,
                           URL:    ci.BaseUrl + "/set",
                           Flags:  HandlerIsSigned | HandlerIsPost,
                           Groups: []string { GroupView, GroupAdmin } }
    handlers = append(handlers, &CrudHandler { desc, ci })

    desc  = &HandlerDesc { Title:  ci.Name,
                           URL:    ci.BaseUrl + "/del",
                           Flags:  HandlerIsSigned | HandlerIsDelete,
                           Groups: []string { GroupView, GroupAdmin } }
    handlers = append(handlers, &CrudHandler { desc, ci })

    return ci.Cruder.RegisterHandlers(ci, handlers)
}

func (p *CrudHandler) Handler() *HandlerDesc {
    return p.HandlerDesc
}


//------------------------------

var mTemplateCrudMap = map[string]*template.Template {}

func (p *CrudHandler) CreateTab(r *http.Request,
                                c appengine.Context,
                                a *AccessInfo) template.HTML {
    ci := p.CrudInfo

    if !ci.Cruder.CanCreateTab(p, r, c, a) {
        return template.HTML(`<em>These are not the droids you are looking for.</em>`)
    }

    t, ok := mTemplateCrudMap[ci.Name]

    if !ok {
        t = template.Must(template.New(ci.TemplateName).ParseFiles("serv/" + ci.TemplateName))
        mTemplateCrudMap[ci.Name] = t
    }

    var buf bytes.Buffer
    err := t.Execute(&buf, struct { Title string } { p.Handler().Title } )
    if err == nil {
        return template.HTML(buf.String())
    }

    return template.HTML("internal tab template error: " + err.Error())
}

//------------------------------

// Note: made public for unit-tests
type CrudEntityLoadSaver struct {
    Value   reflect.Value
}

// implement type PropertyLoadSaver interface
func (e *CrudEntityLoadSaver) Load(c<-chan datastore.Property) error {
    // Load means we consume datastore Properties and fill the crud-client struct
    // specified in e.Value

    // vdst is the destination struct. tdst is its type.
    vdst := e.Value
    if vdst.Kind() == reflect.Ptr {
        vdst = vdst.Elem()
    }
    tdst := vdst.Type()
    vname := tdst.Name()
    if vdst.Kind() != reflect.Struct {
        panic(fmt.Sprintf("CRUD Prop Loader [%s]: type %v found, struct was expected.",
            vname, vdst.Kind()))
    }

    errStr := ""

    for p := range c {
        name := p.Name
        vsrc := p.Value
        tsrc := reflect.TypeOf(vsrc)
        mult := p.Multiple  // requires slice

        //--log.Printf("[CrudEntityLoadSaver.load] %v => %#v -- mult: %v\n", name, vsrc, mult)

        // Check dst has the field name
        var fv reflect.Value
        for i := 0; i < vdst.NumField(); i++ {
            // TODO dst type can have "datastore" field tags to rename their fields
            fs := tdst.Field(i)     // reflect.StructField
            tag := strings.Split(fs.Tag.Get("datastore"), ",")
            fname := fs.Name
            if len(tag) > 0 && tag[0] != "" {
                fname = tag[0]
                if fname == "-" {
                    // datastore tag says to skip this field name
                    //--log.Printf("##DEBUG LOAD skip field name %v with tag %v", fs.Name, tag)
                    continue
                }
            }

            if name == fname {
                // fv is the value of the destination struct field. ft is its type.
                fv = vdst.Field(i)
                break
            }
        }
        if !fv.IsValid() {
            errStr += fmt.Sprintf("CRUD Prop Loader [%s]: No matching field found in %v\n",
                vname, name)
        }

        ft := fv.Type()
        isSlice := ft.Kind() == reflect.Slice

        if isSlice != mult {
            // Warn+Ignore the mismatch
            errStr += fmt.Sprintf("CRUD Prop Loader [%s.%s]: src is slice, dst is %v\n",
                vname, name, ft.Kind())
        } else if !fv.CanSet() {
            // Warn+Ignore
            errStr += fmt.Sprintf("CRUD Prop Loader [%s.%s]: dst is not setable\n", vname, name)
        } else {
            // load into the field or the slice

            var vslice reflect.Value
            if isSlice {
                // A slice is represented by a serie of Property on the same name.
                // Just create a new value on the slice and set it below.

                // Note: new returns a Ptr to the requested type, use Elem() to dereference.
                vptr := reflect.New(ft.Elem())
                // We'll now update a new element, not the slice itself.
                // We'll update the slice only at the end on success.
                vslice = fv
                fv = vptr.Elem()
                ft = fv.Type()
            }

            // Assign to dest value. We don't support all the Property.Value types.
            mismatch := false
            switch ft.Kind() {
            case reflect.Bool:
                if s, ok := vsrc.(bool); ok {
                    fv.SetBool(s)
                } else {
                    mismatch = true
                }
            case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                // datastore property type is an int64. Might overflow when set.
                if s, ok := vsrc.(int64); ok {
                    fv.SetInt(s)
                } else {
                    mismatch = true
                }
            case reflect.Float32, reflect.Float64:
                // datastore property type is a float64. Might overflow when set.
                if s, ok := vsrc.(float64); ok {
                    fv.SetFloat(s)
                } else {
                    mismatch = true
                }
            case reflect.String:
                // Could be an actual "short" string or a datastore BlobKey
                if s, ok := vsrc.(appengine.BlobKey); ok {
                    fv.SetString(string(s))
                    break
                }
                if s, ok := p.Value.(string); ok {
                    fv.SetString(s)
                } else {
                    mismatch = true
                }
            case reflect.Struct:
                // Only struct we support is time.Time
                if s, ok := vsrc.(time.Time); ok {
                    fv.Set(reflect.ValueOf(s))
                } else {
                    mismatch = true
                }
            case reflect.Slice, reflect.Ptr:
                // TODO implement later if useful.
                // slice would map to a []byte for a binary blob
                errStr += fmt.Sprintf("CRUD Prop Loader [%s.%s]: Not-implemented-yet struct type %v",
                    vname, name, ft.Name())

                isSlice = false  // prevent from adding to current slice if any.
            }
            if (mismatch) {
                errStr += fmt.Sprintf("CRUD Prop Loader [%s.%s]: datastore type %v incompatible with struct type %v",
                    vname, name, tsrc.Name(), ft.Name())
            } else if isSlice {
                // If it's a slice, add the new element to the slice
                vslice.Set(reflect.Append(vslice, fv))
            }
        }

    }

    if errStr != "" {
        return errors.New(errStr)
    }
    return nil
}

func (e *CrudEntityLoadSaver) Save(c chan<- datastore.Property) error {
    // Save means we consume the crud-client struct specified in e.Value
    // and produce datastore Properties.

    // Important: we need to close the channel in all conditions on exit.
    defer close(c)

    vsrc := e.Value
    //--log.Printf("##DEBUG SAVER vsrc: %#v", vsrc)
    if vsrc.Kind() == reflect.Ptr {
        vsrc = vsrc.Elem()
    }
    //--log.Printf("##DEBUG SAVER vsrc: %#v", vsrc)
    if vsrc.Kind() == reflect.Interface {
        vsrc = vsrc.Elem()
    }
    //--log.Printf("##DEBUG SAVER vsrc: %#v", vsrc)
    tsrc := vsrc.Type()
    vname := tsrc.Name()
    if vsrc.Kind() != reflect.Struct {
        panic(fmt.Sprintf("CRUD Prop Saver [%s]: type %v found, struct was expected.",
            vname, vsrc.Kind()))
    }

    errStr := ""

    for i := 0; i < vsrc.NumField(); i++ {
        fs := tsrc.Field(i)     // reflect.StructField (aka field descriptor)
        tag := strings.Split(fs.Tag.Get("datastore"), ",")

        // src type can have "datastore" field tags to rename their fields
        name := fs.Name
        if len(tag) > 0 && tag[0] != "" {
            name = tag[0]
            if name == "-" {
                // datastore tag says to skip this field name
                //--log.Printf("##DEBUG SAVE skip field name %v with tag %v", fs.Name, tag)
                continue
            }
        }

        // fv is the value of the source struct field. ft is its type.
        fv := vsrc.Field(i)
        ft := fv.Type()
        if !fv.IsValid() {
            errStr += fmt.Sprintf("CRUD Prop Saver [%s.%s]: Invalid field %v\n",
                vname, name, ft.Name())
            continue
        }

        p := datastore.Property{
            Name:     name,
            NoIndex:  len(tag) == 2 && tag[1] == "noindex",
            Multiple: false,
        }

        if fv.Kind() == reflect.Slice {
            p.Multiple = true

            for j := 0; j < fv.Len(); j++ {
                v := fv.Index(j)

                //--log.Printf("##DEBUG Slice[%v], kind %v, %v", j, v.Kind(), v)

                p.Value = nil
                switch v.Interface().(type) {
                case appengine.BlobKey:
                    p.Value = v
                case time.Time:
                    p.Value = v
                case []byte:
                    p.NoIndex = true
                    p.Value = v
                default:
                    switch v.Kind() {
                    case reflect.Bool:
                        p.Value = v.Bool()
                    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                        p.Value = v.Int()
                    case reflect.Float32, reflect.Float64:
                        p.Value = v.Float()
                    case reflect.String:
                        p.Value = v.String()
                    }
                }
                if p.Value == nil {
                    errStr += fmt.Sprintf("CRUD Prop Saver [%s.%s]: Unsupported slice type %v\n",
                        vname, name, ft.Name())
                    break
                } else {
                    //--log.Printf("##DEBUG SAVE slice %#v", p)
                    c <- p
                }
            }

        } else {
            switch fv.Interface().(type) {
            case appengine.BlobKey:
                p.Value = fv
            case []byte:
                p.NoIndex = true
                p.Value = fv
            default:
                switch fv.Kind() {
                case reflect.Bool:
                    p.Value = fv.Bool()
                case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                    p.Value = fv.Int()
                case reflect.Float32, reflect.Float64:
                    p.Value = fv.Float()
                case reflect.String:
                    p.Value = fv.String()
                }
            }
            if p.Value == nil {
                errStr += fmt.Sprintf("CRUD Prop Saver [%s.%s]: Unsupported type %v\n",
                    vname, name, ft.Name())
            } else {
                //--log.Printf("##DEBUG SAVE value %#v", p)
                c <- p
            }
        }
    }

    if errStr != "" {
        return errors.New(errStr)
    }
    return nil
}

//------------------------------

func (p *CrudHandler) ProcessRequest(w http.ResponseWriter,
                                     r *http.Request,
                                     c appengine.Context,
                                     a *AccessInfo,
                                     urlBase string,
                                     urlRest string) (httpCode int, err error) {
    ci := p.CrudInfo

    if r.Method == "GET" && urlBase == ci.BaseUrl + "/get" {
        return p.processJsonGetList(w, r, c, a)

    } else if r.Method == "POST" && urlBase == ci.BaseUrl + "/set" {
        return p.processJsonAdd(w, r, c, a, urlRest)

    } else if r.Method == "DELETE" && urlBase == ci.BaseUrl + "/del" {
        return p.processJsonDel(w, r, c, a, urlRest)

    } else if pr, ok := ci.Cruder.(CruderProcessRequester); ok {
        return pr.ProcessRequest(w, r, c, a, urlBase, urlRest)
    }

    return http.StatusNotFound, nil
}

// Returns the first struct field with a tag `crud:"key"`.
// If the interface is nil, defaults to CrudInfo.DsStruct which has zero value.
func (ci *CrudInfo) GetCrudKey(d *Datastorer) (name string, value interface{}) {
    if d != nil {
        // Use the given Datastorer to retrieve the key and its value
        v := reflect.ValueOf(d)
        //--log.Printf("##DEBUG 1: %v => %#v", v.Kind(), v)
        if v.Kind() == reflect.Ptr {
            // A pointer was given, get the referenced element
            v = v.Elem()
        }
        //--log.Printf("##DEBUG 2: %v => %#v", v.Kind(), v)
        if v.Kind() == reflect.Interface {
            v = v.Elem()
        }
        //--log.Printf("##DEBUG 3: %v => %#v", v.Kind(), v)
        t := v.Type()
        if v.Kind() != reflect.Struct {
            panic(fmt.Sprintf("getCrudKey: struct expected but %s has type %v : %v", t.Name(), v.Kind(), v.String()))
        }
        for i := 0; i < v.NumField(); i++ {
            fv := v.Field(i)
            ft := t.Field(i)
            if ft.Tag.Get("crud") == "key" {
                return ft.Name, fv.Interface()
            }
        }
        panic(fmt.Sprintf("No field tag crud:\"key\" in struct %s", t.Name()))
    }

    // Otherwise use the default DsSliceType to find the key. No value can be given.
    t := reflect.TypeOf(ci.DsStruct)
    if t.Kind() == reflect.Slice {
        // Get the underlying type of the slice.
        t = t.Elem()
    }
    if t.Kind() == reflect.Ptr {
        // Get the underlying type of the pointer.
        t = t.Elem()
    }
    if t.Kind() != reflect.Struct {
        panic(fmt.Sprintf("getCrudKey: struct expected but %s has type %v", t.Name(), t.Kind()))
    }
    for i := 0; i < t.NumField(); i++ {
        ft := t.Field(i)
        if ft.Tag.Get("crud") == "key" {
            return ft.Name, ""
        }
    }
    panic(fmt.Sprintf("No field tag crud:\"key\" in struct %s", t.Name()))
    return "", ""
}


func (p *CrudHandler) processJsonGetList(w http.ResponseWriter,
                                         r *http.Request,
                                         c appengine.Context,
                                         a *AccessInfo) (httpCode int, err error) {
    ci := p.CrudInfo

    //--TODO-- defer AddLogNow(c, u, kAccessKind, 0, LogActionView)

    // Check user ability. Must be in group View to list content.
    if !a.Is(GroupView) {
        return http.StatusUnauthorized, nil
    }

    v := r.URL.Query()

    // dataTable requires this argument to be passed through as an int
    echo := v.Get("sEcho")
    if _, err = strconv.Atoi(echo); err != nil {
        echo = "invalid"
    }

    // dataTable way of specifying pagination
    minRange, _ := strconv.Atoi(v.Get("iDisplayStart"))
    lenRange, _ := strconv.Atoi(v.Get("iDisplayLength"))

    //--log.Printf("## GET-LIST range=%v+%v", minRange, lenRange); // DEBUG

    key, _ := ci.GetCrudKey(nil)

    q := datastore.NewQuery(ci.Kind).Order(key)
    var n int
    if n, err = q.Count(c); err == nil {
        // Adjust query min/max based on range requested and actual count
        imin := Choice(minRange > 0, minRange, 0)
        ilen := Choice(imin + lenRange <= n, lenRange, n - imin)

        q.Offset(imin).Limit(ilen);

        var data struct {
            SEcho       string              `json:"sEcho"`
            TotalNum    int                 `json:"iTotalRecords"`
            FilterNum   int                 `json:"iTotalDisplayRecords"`
            AAData      [][]string          `json:"aaData"`
        }
        data.AAData = make([][]string, 0, ilen)

        t := reflect.TypeOf(ci.DsStruct)

        for it := q.Run(c); ; {
            v := CrudEntityLoadSaver { reflect.New(t).Elem() }

            _, err = it.Next(&v) // will call v.Load(<-data_channel)
            if err != nil {
                break
            }
            entry := v.Value.Interface().(Datastorer)
            data.AAData = append(data.AAData, entry.ToJsonTable())
        }
        if err == datastore.Done || err == nil {
            err = nil
            data.SEcho = echo
            data.TotalNum = n
            data.FilterNum = n

            //--log.Printf("## GET-LIST reply %v - %v / %v -- %v", imin, imin+ilen, n, data);

            if b, err := json.MarshalIndent(data, "", "    "); err == nil {
                w.Header().Add("Content-Range", fmt.Sprintf("items %d-%d/%d", imin, imin+ilen, n))
                w.WriteHeader(http.StatusOK)
                _, err = w.Write(b)

                // Return 0 to avoid having the parent trying to set header when there's no error.
                return 0, err
            }
        }
    }

    log.Println("processJsonGetList err: ", err)
    return http.StatusInternalServerError, err
}


func (p *CrudHandler) processJsonAdd(w http.ResponseWriter,
                                    r *http.Request,
                                    c appengine.Context,
                                    a *AccessInfo,
                                    urlRest string) (httpCode int, err error) {
    ci := p.CrudInfo

    // Check user ability. Must be in group Create.
    if !a.Is(GroupCreate) {
        return http.StatusUnauthorized, nil
    }

    blob, err := ioutil.ReadAll(r.Body)
    if err != nil {
        return http.StatusInternalServerError, err
    }

    v, err := url.ParseQuery(string(blob))
    if err == nil {
        var info Datastorer
        info, err = ci.Cruder.ValidateAdd(v)

        if err != nil {
            //--log.Printf("##DEBUG PUT-ERROR: %v", err.Error())
            return http.StatusBadRequest, err
        }

        //--log.Printf("##DEBUG PUT-INFO: [%v %v] %#v",
        //    reflect.ValueOf(info).Kind(),
        //    reflect.TypeOf(info),
        //    info)

        // FIXME: both lines might panic. Try and generate an Error
        _, vkey := ci.GetCrudKey(&info)
        skey := vkey.(string)

        //--log.Printf("## PUT-ITEM data=%v", info);

        key := datastore.NewKey(c, ci.Kind, skey, 0, nil)

        saver := &CrudEntityLoadSaver { reflect.ValueOf(info) }
        //--log.Printf("##DEBUG saver=%#v", saver);
        if _, err = datastore.Put(c, key, saver); err == nil {
            //--log.Printf("## PUT-ITEM datastore NO ERROR");
            return http.StatusOK, nil
        }
        //--log.Printf("## PUT-ITEM datastore ERROR: %v", err.Error());
    }

    return http.StatusBadRequest, err
}


func (p *CrudHandler) processJsonDel(w http.ResponseWriter,
                                    r *http.Request,
                                    c appengine.Context,
                                    a *AccessInfo,
                                    urlRest string) (httpCode int, err error) {
    ci := p.CrudInfo

    // Check user ability. Must be in group Create.
    if !a.Is(GroupCreate) {
        return http.StatusUnauthorized, nil
    }

    // urlRest should be /<key>.
    f := strings.Split(urlRest, "/")
    if len(f) == 2 {
        //--log.Printf("## DELETE-ITEM key=%s", f[1]); // DEBUG

        key := datastore.NewKey(c, ci.Kind, f[1], 0, nil)
        if err = datastore.Delete(c, key); err == datastore.ErrNoSuchEntity  {
            return http.StatusNotFound, err
        }
        // Return 0 to avoid having the parent trying to set header when there's no error.
        return 0, err
    }

    log.Println("processJsonDel err: ", urlRest, err)
    return http.StatusNotFound, err
}
